/*
**    Copyright (C) 2003-2011 Institute for Systems Biology
**                            Seattle, Washington, USA.
**
**    This library is free software; you can redistribute it and/or
**    modify it under the terms of the GNU Lesser General Public
**    License as published by the Free Software Foundation; either
**    version 2.1 of the License, or (at your option) any later version.
**
**    This library is distributed in the hope that it will be useful,
**    but WITHOUT ANY WARRANTY; without even the implied warranty of
**    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
**    Lesser General Public License for more details.
**
**    You should have received a copy of the GNU Lesser General Public
**    License along with this library; if not, write to the Free Software
**    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
*/

package org.systemsbiology.hadoop.ioformats;

import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;

/**
 * Created by IntelliJ IDEA.
 * User: anorberg
 * Date: 9/21/11
 * Time: 3:13 PM
 *
 * A class that holds an order-sensitive list of strings, and uses them as a Hadoop key.
 * Does not actually sort in lexicographic order; the actual order logic is hash-based, for efficiency reasons.
 * Expect a consistent but nigh-meaningless order.
 */
public class LexicalKey implements WritableComparable<LexicalKey> {
    private String[] content;
    private long comparisonRoot;

    private static final long WRAP_FIX = 0L - ((long)Integer.MIN_VALUE) - ((long)Integer.MIN_VALUE);
    private static final int MAGIC_SIGNATURE_OPEN = 0x3E3E3E3E;
    private static final int MAGIC_SIGNATURE_CLOSE = 0xC0DED0D0;

    private static final boolean PARANOID = true;

    public LexicalKey(){
        content = new String[0];
        comparisonRoot = 0;
    }

    public LexicalKey(String[] data){
        content = new String[data.length];
        System.arraycopy(data, 0, content, 0, data.length);
        comparisonRoot = shiftHash(data.length - 1);
    }

    public LexicalKey(Iterable<String> data){
        ArrayList<String> copy = new ArrayList<String>();
        for(String s: data){
            copy.add(s);
        }
        content = copy.toArray(new String[copy.size()]);
        comparisonRoot = shiftHash(content.length - 1);
    }

    /**
     * Creates a comparison hash value from the contents of the array.
     * @param index
     * @return
     */
    private long shiftHash(int index){
        if(index < 0) return 0;
        long extended = content[index].hashCode();
        if(extended < 0){ extended += WRAP_FIX; }
        long remainingHash = shiftHash(index-1);
        if((remainingHash & 0xF000000000000000L) != 0L){
            extended += 1;
        }
        return (remainingHash << 1) + extended;
    }

    public int compareTo(LexicalKey x){
        if(comparisonRoot < x.comparisonRoot){
            return -1;
        }
        if(comparisonRoot > x.comparisonRoot){
            return 1;
        }

        //deep equality check
        for(int i = 0; i < x.content.length || i < content.length ; ++i){
            if(i >= content.length){
                return - 1;
            }
            if(i >= x.content.length){
                return 1;
            }
            int q = content[i].compareTo(x.content[i]);
            if(q != 0) return q;
        }
        return 0;
    }

    public int size(){
        return content.length;
    }

    public String get(int index){
        return content[index];
    }

    @Override
    public boolean equals(Object o){
        if(o == null) return false;
        if(o == this) return true; //early-out, later tests are slow
        if(o.getClass() != this.getClass()) return false;
        return compareTo((LexicalKey)o) == 0;
    }

    @Override
    public int hashCode(){
        return (int) (comparisonRoot ^ (comparisonRoot >>> 32));
    }

    //DATA FORMAT:
    //magic number open
    //number of elements
    //that many UTF-8 strings
    //the long comparison root (a checksum)
    //magic number close
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeInt(MAGIC_SIGNATURE_OPEN);
        dataOutput.writeInt(content.length);
        for(String s: content){
            dataOutput.writeUTF(s);
        }
        dataOutput.writeLong(comparisonRoot);
        dataOutput.writeInt(MAGIC_SIGNATURE_CLOSE);
    }

    public void readFields(DataInput dataInput) throws IOException {
        if(dataInput.readInt() != MAGIC_SIGNATURE_OPEN){
            throw new IOException("Failed to match opening signature");
        }

        int n = dataInput.readInt();
        if(content.length != n){
            content = new String[n];
        }
        for(int k = 0; k < n; ++k){
            content[k] = dataInput.readUTF();
        }
        comparisonRoot = dataInput.readLong();
        if(PARANOID){
            long verify = shiftHash(content.length - 1);
            if(verify != comparisonRoot){
                throw new IOException("Checksum (via comparison root) failed!");
            }
        }
        if(dataInput.readInt() != MAGIC_SIGNATURE_CLOSE){
            throw new IOException("Failed to match closing signature");
        }
    }

    @Override
    /**
     * Provide a clean, tab-delimited representation of the compound key.
     */
    public String toString(){
        if(content == null || content.length == 0){
            return "";
        }
        StringBuilder out = new StringBuilder(content[0]);
        for(int k = 1; k < content.length; ++k){
            out.append("\t");
            out.append(content[k]);
        }
        return out.toString();
    }
}
